#region Copyright & License

//
// Author: Ian Davis <ian.f.davis@gmail.com> Copyright (c) 2007, Ian Davs
//
// Portions of this software were developed for NUnit. See NOTICE.txt for more
// information. 
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may not
// use this file except in compliance with the License. You may obtain a copy of
// the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
// License for the specific language governing permissions and limitations under
// the License.
//

#endregion

using System;
using System.Collections;
using System.Collections.Generic;

namespace Ensurance.Constraints
{
    /// <summary>
    /// ConstraintBuilder is used to resolve the Not and All properties, which
    /// serve as prefix operators for constraints. With the addition of an
    /// operand stack, And and Or could be supported, but we have left them out
    /// in favor of a simpler, more type-safe implementation. Use the &amp; and
    /// | operator overloads to combine constraints.
    /// </summary>
    public class ConstraintBuilder
    {
        private Stack<string> _opnds = new Stack<string>();
        private Stack<Op> _ops = new Stack<Op>();

        /// <summary>
        /// Implicitly convert ConstraintBuilder to an actual Constraint at the
        /// point where the syntax demands it.
        /// </summary>
        /// <param name="builder"></param>
        /// <returns></returns>
        public static implicit operator Constraint( ConstraintBuilder builder )
        {
            return builder.Resolve();
        }

        #region Constraints Without Arguments

        /// <summary>
        /// Resolves the chain of constraints using EqualConstraint(null) as
        /// base.
        /// </summary>
        public Constraint Null
        {
            get { return Resolve( new EqualConstraint( null ) ); }
        }

        /// <summary>
        /// Resolves the chain of constraints using EqualConstraint(true) as
        /// base.
        /// </summary>
        public Constraint True
        {
            get { return Resolve( new EqualConstraint( true ) ); }
        }

        /// <summary>
        /// Resolves the chain of constraints using EqualConstraint(false) as
        /// base.
        /// </summary>
        public Constraint False
        {
            get { return Resolve( new EqualConstraint( false ) ); }
        }

        /// <summary>
        /// Resolves the chain of constraints using Is.NaN as base.
        /// </summary>
        public Constraint NaN
        {
            get { return Resolve( new EqualConstraint( double.NaN ) ); }
        }

        /// <summary>
        /// Resolves the chain of constraints using Is.Empty as base.
        /// </summary>
        public Constraint Empty
        {
            get { return Resolve( new EmptyConstraint() ); }
        }

        /// <summary>
        /// Resolves the chain of constraints using Is.Unique as base.
        /// </summary>
        public Constraint Unique
        {
            get { return Resolve( new UniqueItemsConstraint() ); }
        }

        #endregion

        #region Constraints with an expected value

        #region Equality and Identity

        /// <summary>
        /// Resolves the chain of constraints using an EqualConstraint as base.
        /// </summary>
        public Constraint EqualTo( object expected )
        {
            return Resolve( new EqualConstraint( expected ) );
        }

        /// <summary>
        /// Resolves the chain of constraints using a SameAsConstraint as base.
        /// </summary>
        public Constraint SameAs( object expected )
        {
            return Resolve( new SameAsConstraint( expected ) );
        }

        #endregion

        #region Comparison Constraints

        /// <summary>
        /// Resolves the chain of constraints using a LessThanConstraint as
        /// base.
        /// </summary>
        public Constraint LessThan( IComparable expected )
        {
            return Resolve( new LessThanConstraint( expected ) );
        }

        /// <summary>
        /// Resolves the chain of constraints using a GreaterThanConstraint as
        /// base.
        /// </summary>
        public Constraint GreaterThan( IComparable expected )
        {
            return Resolve( new GreaterThanConstraint( expected ) );
        }

        /// <summary>
        /// Resolves the chain of constraints using a LessThanOrEqualConstraint
        /// as base.
        /// </summary>
        public Constraint LessThanOrEqualTo( IComparable expected )
        {
            return Resolve( new LessThanOrEqualConstraint( expected ) );
        }

        /// <summary>
        /// Resolves the chain of constraints using a LessThanOrEqualConstraint
        /// as base.
        /// </summary>
        public Constraint AtMost( IComparable expected )
        {
            return Resolve( new LessThanOrEqualConstraint( expected ) );
        }

        /// <summary>
        /// Resolves the chain of constraints using a
        /// GreaterThanOrEqualConstraint as base.
        /// </summary>
        public Constraint GreaterThanOrEqualTo( IComparable expected )
        {
            return Resolve( new GreaterThanOrEqualConstraint( expected ) );
        }

        /// <summary>
        /// Resolves the chain of constraints using a
        /// GreaterThanOrEqualConstraint as base.
        /// </summary>
        public Constraint AtLeast( IComparable expected )
        {
            return Resolve( new GreaterThanOrEqualConstraint( expected ) );
        }

        #endregion

        #region Type Constraints

        /// <summary>
        /// Resolves the chain of constraints using an ExactTypeConstraint as
        /// base.
        /// </summary>
        public Constraint TypeOf( Type expectedType )
        {
            return Resolve( new ExactTypeConstraint( expectedType ) );
        }

        /// <summary>
        /// Resolves the chain of constraints using an InstanceOfTypeConstraint
        /// as base.
        /// </summary>
        public Constraint InstanceOfType( Type expectedType )
        {
            return Resolve( new InstanceOfTypeConstraint( expectedType ) );
        }

        /// <summary>
        /// Resolves the chain of constraints using an AssignableFromConstraint
        /// as base.
        /// </summary>
        public Constraint AssignableFrom( Type expectedType )
        {
            return Resolve( new AssignableFromConstraint( expectedType ) );
        }

        #endregion

        #region Containing Constraint

        /// <summary>
        /// Resolves the chain of constraints using a ContainsConstraint as
        /// base. This constraint will, in turn, make use of the appropriate
        /// second-level constraint, depending on the type of the actual
        /// argument.
        /// </summary>
        public Constraint Contains( object expected )
        {
            return Resolve( new ContainsConstraint( expected ) );
        }

        /// <summary>
        /// Resolves the chain of constraints using a 
        /// CollectionContainsConstraint as base.
        /// </summary>
        /// <param name="expected">The expected object</param>
        public Constraint Member( object expected )
        {
            return Resolve( new CollectionContainsConstraint( expected ) );
        }

        #endregion

        #region String Constraints

        /// <summary>
        /// Resolves the chain of constraints using a StartsWithConstraint as
        /// base.
        /// </summary>
        public Constraint StartsWith( string substring )
        {
            return Resolve( new StartsWithConstraint( substring ) );
        }

        /// <summary>
        /// Resolves the chain of constraints using a StringEndingConstraint as
        /// base.
        /// </summary>
        public Constraint EndsWith( string substring )
        {
            return Resolve( new EndsWithConstraint( substring ) );
        }

        /// <summary>
        /// Resolves the chain of constraints using a StringMatchingConstraint
        /// as base.
        /// </summary>
        public Constraint Matches( string pattern )
        {
            return Resolve( new RegexConstraint( pattern ) );
        }

        #endregion

        #region Collection Constraints

        /// <summary>
        /// Resolves the chain of constraints using a
        /// CollectionEquivalentConstraint as base.
        /// </summary>
        public Constraint EquivalentTo( ICollection expected )
        {
            return Resolve( new CollectionEquivalentConstraint( expected ) );
        }

        /// <summary>
        /// Resolves the chain of constraints using a
        /// CollectionContainingConstraint as base.
        /// </summary>
        public Constraint CollectionContaining( object expected )
        {
            return Resolve( new CollectionContainsConstraint( expected ) );
        }

        /// <summary>
        /// Resolves the chain of constraints using a CollectionSubsetConstraint
        /// as base.
        /// </summary>
        public Constraint SubsetOf( ICollection expected )
        {
            return Resolve( new CollectionSubsetConstraint( expected ) );
        }

        #endregion

        #region Property Constraints

        /// <summary>
        /// Resolves the chain of constraints using a PropertyConstraint as base
        /// </summary>
        public Constraint Property( string name, object expected )
        {
            return Resolve( new PropertyConstraint( name, new EqualConstraint( expected ) ) );
        }

        /// <summary>
        /// Resolves the chain of constraints using a PropertyCOnstraint on
        /// Length as base
        /// </summary>
        /// <param name="length"></param>
        /// <returns></returns>
        public Constraint Length( int length )
        {
            return Property( "Length", length );
        }

        /// <summary>
        /// Resolves the chain of constraints using a PropertyCOnstraint on
        /// Length as base
        /// </summary>
        /// <param name="count"></param>
        /// <returns></returns>
        public Constraint Count( int count )
        {
            return Property( "Count", count );
        }

        #endregion

        #endregion

        #region Prefix Operators

        /// <summary>
        /// Modifies the ConstraintBuilder by pushing a Not operator on the
        /// stack.
        /// </summary>
        public ConstraintBuilder Not
        {
            get
            {
                _ops.Push( Op.Not );
                return this;
            }
        }

        /// <summary>
        /// Modifies the ConstraintBuilder by pushing a Not operator on the
        /// stack.
        /// </summary>
        public ConstraintBuilder No
        {
            get
            {
                _ops.Push( Op.Not );
                return this;
            }
        }

        /// <summary>
        /// Modifies the ConstraintBuilder by pushing an All operator on the
        /// stack.
        /// </summary>
        public ConstraintBuilder All
        {
            get
            {
                _ops.Push( Op.All );
                return this;
            }
        }

        /// <summary>
        /// Modifies the ConstraintBuilder by pushing a Some operator on the
        /// stack.
        /// </summary>
        public ConstraintBuilder Some
        {
            get
            {
                _ops.Push( Op.Some );
                return this;
            }
        }

        /// <summary>
        /// Modifies the constraint builder by pushing All and Not operators on
        /// the stack
        /// </summary>
        public ConstraintBuilder None
        {
            get
            {
                _ops.Push( Op.None );
                return this;
            }
        }

        /// <summary>
        /// Modifies the ConstraintBuilder by pushing a Prop operator on the ops
        /// stack and the name of the property on the opnds stack.
        /// </summary>
        /// <param name="name"></param>
        /// <returns></returns>
        public ConstraintBuilder Property( string name )
        {
            _ops.Push( Op.Prop );
            _opnds.Push( name );
            return this;
        }

        #endregion

        #region Helper Methods

        /// <summary>
        /// Resolve a constraint that has been recognized by applying any
        /// pending operators and returning the resulting Constraint.
        /// </summary>
        /// <returns>A constraint that incorporates all pending operators</returns>
        private Constraint Resolve( Constraint constraint )
        {
            while ( _ops.Count > 0 )
            {
                switch ( _ops.Pop() )
                {
                    case Op.Not:
                        constraint = new NotConstraint( constraint );
                        break;
                    case Op.All:
                        constraint = new AllItemsConstraint( constraint );
                        break;
                    case Op.Some:
                        constraint = new SomeItemsConstraint( constraint );
                        break;
                    case Op.None:
                        constraint = new NoItemConstraint( constraint );
                        break;
                    case Op.Prop:
                        constraint = new PropertyConstraint( _opnds.Pop(), constraint );
                        break;
                }
            }

            return constraint;
        }

        private Constraint Resolve()
        {
            return Resolve( null );
        }

        #endregion

        #region Nested type: Op

        private enum Op
        {
            Not,
            All,
            Some,
            None,
            Prop,
        }

        #endregion
    }
}